#!/usr/bin/env perl
use warnings;
use strict;
#
#
#

package Symbols;
use warnings;
use strict;

sub new($) {
    my ($class) = @_;
    my $self = {};
    bless $self, $class;

    return $self;
}

sub add {
    my $self = shift;
    my $addr = shift;
    my $name = shift;

    die("bad symbol add $addr $name") if (!defined($name) or !defined($addr));

    # dont overwrite an existing name
    if (!defined($self->{a2n}{$addr})) {
        $self->{a2n}{$addr} = $name;
    }
    return $self;
}

sub lookup_addr {
    my $self = shift;
    my $addr = shift;

    return $self->{a2n}{$addr};
}

sub all_addrs {
    my $self = shift;
    return sort {$a <=> $b} keys(%{$self->{a2n}});
}

sub addr2str {
    my $self = shift;
    my $addr = shift;

    my $name = $self->lookup_addr($addr);
    if (!defined($name)) {
        $name = sprintf("sym_%08x",$addr);
    }
    return $name;
}

1;

package MemRegions;
use warnings;
use strict;

use IO::File;

my $debug = 0;

sub new($) {
    my ($class) = @_;
    my $self = {};
    bless $self, $class;

    return $self;
}

sub add {
    my $self = shift;
    my ($phys_addr, $size, $filename, $file_offset, $flags) = @_;

    die("bad MemRegion add") if (
        !defined($phys_addr) || !defined($size) ||
        !defined($filename) || !defined($file_offset)
    );

    my $region;
    $region->{phys_addr}   = $phys_addr;
    $region->{size}        = $size;
    $region->{filename}    = $filename;
    $region->{file_offset} = $file_offset;
    $region->{flags}       = $flags;

    push @{$self->{region}}, $region;

    if ($flags & 2) {
        # anonymous memory has no file backing
        return;
    }

    my $fh = IO::File->new($filename, O_RDONLY);
    if (!defined($fh)) {
        warn("Could not open $filename\n");
        exit(1);
    }
    $region->{fh} = $fh;
}

sub _addr2region {
    my $self = shift;
    my $phys_addr = shift;
    my $size = shift;

    my $region;
    # find the correct region
    for my $r (@{$self->{region}}) {
        if ($phys_addr >= $r->{phys_addr} && $phys_addr <= $r->{phys_addr}+$r->{size}) {
            $region = $r;
            last;
        }
    }
    return $region;
}

sub read {
    my $self = shift;
    my $phys_addr = shift;
    my $size = shift;

    my $region = $self->_addr2region($phys_addr,$size);
    if (!defined($region)) {
        printf("unhandled address 0x%08x(0x%x)\n",$phys_addr,$size);
        return undef;
    }

    my $offset = $phys_addr - $region->{phys_addr};
    $offset += $region->{file_offset};

    if ($debug) {
        printf("0x%08x(%x) = 0x%08x (%s)\n",
            $phys_addr,$size,
            $offset,
            $region->{filename},
        );
    }

    if ($region->{flags} & 2) {
        # anonymous memory
        return chr(0)x$size;
    }

    $region->{fh}->seek($offset,SEEK_SET);
    my $buf;
    $region->{fh}->read($buf,$size);

    return $buf;
}

sub read_word {
    my $self = shift;
    my $phys_addr = shift;
    my $size = shift;

    my $formatstr;
    if ($self->{endian} eq 'little') {
        if ($size == 2) {
            $formatstr = 'v';
        } elsif ($size == 4) {
            $formatstr = 'V';
        } else {
            ...;
        }
    } else {
        # must be 'big'
        if ($size == 2) {
            $formatstr = 'n';
        } elsif ($size == 4) {
            $formatstr = 'N';
        } else {
            ...;
        }
    }

    my $buf = $self->read($phys_addr,$size);
    return undef if (length($buf) != $size);

    return unpack($formatstr,$buf);
}

sub set_endian {
    my $self = shift;
    my $endian = shift;

    if ($endian eq 'little' || $endian eq 'big') {
        $self->{endian} = $endian;
    } else {
        die ("bad endianness");
    }
}

sub all_baseaddr {
    my $self = shift;
    my @starts;
    for my $r (@{$self->{region}}) {
        push @starts, $r->{phys_addr};
    }
    return @starts;
}

sub region_size {
    my $self = shift;
    my $phys_addr = shift;

    my $size = 1; # a fake size
    my $region = $self->_addr2region($phys_addr,$size);
    if (!defined($region)) {
        return undef;
    }
    return $region->{size};
}

sub region_base {
    my $self = shift;
    my $phys_addr = shift;

    my $size = 1; # a fake size
    my $region = $self->_addr2region($phys_addr,$size);
    if (!defined($region)) {
        return undef;
    }
    return $region->{phys_addr};
}

1;

package main;
use IO::File;

use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Quotekeys = 0;

sub load_configfile {
    my $db = shift;
    my $filename = shift;

    my $fh = IO::File->new($filename, O_RDONLY);
    if (!defined($fh)) {
        warn("Could not open $filename\n");
        exit(1);
    }

    while(<$fh>) {
        chomp; s/\r//g;

        # remove whitespace
        s/^\s+//;

        # remove comment lines
        s/^[#].*//;

        if (m/^include\s+(\S+)/) {
            load_configfile($db,$1);
        } elsif (m/^load_memory\s+/) {
            my @a = split(/\s+/,$_);
            $db->{regions}->add(
                eval "$a[1]", eval "$a[2]", $a[3], eval "$a[4]", $a[5]
            );
        } elsif (m/^f\W+/) {
            my @a = split(/\W+/,$_);
            # 0 1              2 3
            # f table.00021510 1 0x00021510
            $db->{symbols}->add(eval "$a[3]", $a[1]);
        } elsif (m/^sizes\W+/) {
            my @a = split(/\W+/,$_);
            $db->{sizes}{$a[1]} = $a[2];
        } elsif (m/^option\W+/) {
            my @a = split(/\W+/,$_);
            $db->{option}{$a[1]} = $a[2];
        }
    }
}

sub validate_pointer {
    my $db = shift;
    my $val = shift;

    # Check it is correctly aligned
    # FIXME - this only works for binary values of alignment
    if (($val & ($db->{sizes}{align}-1)) != 0) {
        return undef;
    }

    if (!defined($db->{regions}->_addr2region($val,$db->{sizes}{ptr}))) {
        return undef;
    }

    return $val;
}

sub find_pointers {
    my $db = shift;

    for my $start ($db->{regions}->all_baseaddr()) {
        my $end = $start + $db->{regions}->region_size($start);

        $db->{symbols}->add($start,sprintf("region_%08x",$start));

        my $i = $start;
        while (($i+$db->{sizes}{ptr}) <= $end ) {
            my $val = $db->{regions}->read_word($i,$db->{sizes}{ptr});
            if (validate_pointer($db,$val)) {
                $db->{symbols}->add($val,sprintf("ptr_%08x",$val));

                $db->{p}{src}{$i} = $val;

                $i += $db->{sizes}{ptr};
            } else {
                $i += $db->{sizes}{align};
            }
        }
    }
}

sub glom_objects {
    my $db = shift;
    my @addrs = $db->{symbols}->all_addrs();

    while (@addrs) {
        my $addr = shift @addrs;
        my $object;
        $object->{addr} = $addr;
        $db->{p}{obj}{$addr} = $object;

        my $next_addr = $addrs[0];
        $object->{_}{next_addr} = $next_addr;
        if (!defined($next_addr)) {
            $next_addr = $addr;
        }

        my $region_base =  $db->{regions}->region_base($addr);
        $object->{_}{region_base} = $region_base;
        # if we know nothing, then we cannot glom anything
        if (!defined($region_base)) {
            $object->{size} = "-1";
            next;
        }

        my $region_end = $region_base + $db->{regions}->region_size($addr);
        $object->{_}{region_end} = $region_end;
        if ($next_addr > $region_end) {
            $next_addr = $region_end;
        }

        $object->{_}{glom_end} = $next_addr;

        my $size = $next_addr - $addr;
        $object->{size} = $size;

        my $offset = 0;
        while ($addr < $next_addr) {
            # TODO - handle sizeof(word) != sizeof(ptr) and the addr offsets
            # this will cause..

            if (defined($db->{p}{src}{$addr})) {
                $object->{p}{$offset} = $db->{p}{src}{$addr};
                $object->{d}{$offset} = undef;
                $addr += $db->{sizes}{ptr};
                $offset += $db->{sizes}{ptr};
            } else {
                my $val = $db->{regions}->read_word($addr,$db->{sizes}{word});
                $object->{d}{$offset} = $val;
                $addr += $db->{sizes}{word};
                $offset += $db->{sizes}{word};
            }
        }

    }
}

sub output_dot {
    my $db = shift;

    print "digraph structs {\n";
    print " rankdir=LR;\n";
    print " node [shape=record];\n";
    print "\n";

    for my $addr (sort {$a <=> $b} keys(%{$db->{p}{obj}})) {
        my $object = $db->{p}{obj}{$addr};
        my $name = $db->{symbols}->addr2str($addr);

        my @ports;
        push @ports,"<p>$name:";
        for my $offset (sort {$a <=> $b} keys(%{$object->{d}})) {
            my $val = $object->{d}{$offset};
            if (!defined($val)) {
                # this is a pointer
                my $dst = $object->{p}{$offset};
                my $dstname = $db->{symbols}->addr2str($dst);
                if ($dst != $addr) {
                    # dont draw a self reference arrow
                    printf(" %s:p%i -> %s:p;\n",$name,$offset,$dstname);
                } else {
                    # but highlight it
                    $dstname .= "**";
                }
                push @ports, sprintf("<p%i>%s",$offset,$dstname);
            } else {
                my $valname = $db->{symbols}->lookup_addr($val);
                if (!defined($valname)) {
                    $valname = sprintf("0x%08x",$val);
                }
                push @ports, sprintf("<p%i>%s",$offset,$valname);
            }
        }

        # what is the maximum number of rows per object?
        my $want_rows = $db->{option}{want_rows};

        if ($want_rows < 2) {
            @ports = $ports[0];
        } elsif (scalar(@ports) > $want_rows) {
            @ports = @ports[0..$want_rows-2];
            push @ports, sprintf("...size=0x%x",$object->{size});
        }

        printf(" %s [label=\"%s\"]; // %i\n",
            $name,
            join("|",@ports),
            $object->{size}
        );
        printf("\n");
    }
    print "}\n";
}

sub main() {
    my $configfile = shift @ARGV;
    if (!defined($configfile)) {
        die('need configfile');
    }

    my $db = {};

    # Default config values
    $db->{sizes}{align} = 4;
    $db->{sizes}{word} = 4;
    $db->{sizes}{ptr} = 4;
    $db->{sizes}{endian} = "little";
    $db->{option}{want_rows} = 1;

    $db->{symbols} = Symbols->new();
    $db->{regions} = MemRegions->new();

    load_configfile($db,$configfile);

    $db->{regions}->set_endian($db->{sizes}{endian});

    find_pointers($db);
    glom_objects($db);

    if (@ARGV) {
        print Dumper($db);
    }

    output_dot($db);
}
main();

